<?php
/*
Copyright (C) 2014 by Joey Sabey (GameFreak7744@gmail.com)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/

require_once('User.php');

class Key {
    //Version string...
    const VERSION = 'trunk';

    //Flags
    const GET_BY_ID   = 0;
    const GET_BY_NAME = 1;

    //SQL schema
    const KEYS_SCHEMA = <<<SQL
        CREATE TABLE IF NOT EXISTS keys (
            id INTEGER PRIMARY KEY,
            name TEXT UNIQUE NOT NULL,
            description TEXT
        );
SQL;
    const USERKEYS_SCHEMA = <<<SQL
        CREATE TABLE IF NOT EXISTS userKeys (
            id INTEGER PRIMARY KEY,
            userID INTEGER NOT NULL,
            keyID INTEGER NOT NULL,
            FOREIGN KEY (userID) REFERENCES users(id)
                ON DELETE CASCADE
                ON UPDATE CASCADE,
            FOREIGN KEY (keyID) REFERENCES keys(id)
                ON DELETE CASCADE
                ON UPDATE CASCADE
        );
SQL;
    const USERKEYSINDEX_SCHEMA = <<<SQL
        CREATE UNIQUE INDEX IF NOT EXISTS userKeysIndex ON userKeys (
            userID,
            keyID
        );
SQL;

    //Class variables
    protected $id = NULL;
    protected $name = NULL;
    protected $description = NULL;

    //Class constructor; loads Key data from the database by id or name
    public function __construct($uid, $mode = self::GET_BY_ID) {
        $db = self::getDB();
        switch ($mode) {
            case self::GET_BY_ID:
                $query = $db->prepare('SELECT * FROM keys WHERE id = :id');
                $query->bindParam(':id', $uid, PDO::PARAM_INT);
                break;
            case self::GET_BY_NAME:
                $query = $db->prepare('SELECT * FROM keys WHERE name = :name');
                $query->bindParam(':name', $uid, PDO::PARAM_STR);
                break;
            default:
                throw new KeyInvalidModeException('__construct', $mode);
        }
        $query->execute();
        $query->bindColumn('id', $this->id, PDO::PARAM_INT);
        $query->bindColumn('name', $this->name, PDO::PARAM_STR);
        $query->bindColumn('description', $this->description, PDO::PARAM_STR);
        $query->fetch(PDO::FETCH_BOUND);
        if($this->id === NULL)
            throw new KeyNoSuchKeyException($uid, $mode);
    }

    //String representations of Key objects are merely their 'human' name
    public function __toString() {
        return $this->getName();
    }

    //Basic get methods
	public function getID() {
		return $this->id;
	}
	public function getName() {
		return $this->name;
	}
	public function getDescription() {
		return $this->description;
	}

    //Set methods - these methods update both the db and instance representation
    public function setName($name) {
        try {
            $db = self::getDB();
            $query = $db->prepare('UPDATE keys SET name=:name WHERE id=:id');
            $query->bindParam(':name', $name, PDO::PARAM_STR);
            $query->bindParam(':id', $this->id, PDO::PARAM_INT);
            $query->execute();
            $this->name = $name;
        }
        catch(PDOException $e) {
            if($e->getCode() == '23000')
                throw new KeyUnavailableNameException($name, $e);
            throw $e;
        }
    }
    public function setDescription($description) {
        $db = self::getDB();
		$query = $db->prepare('UPDATE keys SET description=:description WHERE id=:id');
		$query->bindParam(':description', $description, PDO::PARAM_STR);
		$query->bindParam(':id', $this->id, PDO::PARAM_INT);
		$query->execute();
		$this->description = $description;
    }

    //This method grants a Key to a User, throwing an exception if they already hold it
    public function give($user) {
        try {
            $db = self::getDB();
            $query = $db->prepare('INSERT INTO userKeys (userID, keyID) VALUES (:userID, :keyID)');
            $query->bindParam(':userID', $user->getID(), PDO::PARAM_INT);
            $query->bindParam(':keyID', $this->id, PDO::PARAM_INT);
            $query->execute();
        }
        catch(PDOException $e) {
            if($e->getCode() == '23000')
                throw new KeyUserAlreadyHasKeyException($user, $this, $e);
            throw $e;
        }
    }

    //This method revokes a Key from a User. Note that it does -not- care if the given User
    //actually -has- the Key or not, at least for now
    public function take($user) {
        $db = self::getDB();
        $query = $db->prepare('DELETE FROM userKeys WHERE userID = :userID AND keyID = :keyID');
        $query->bindParam(':userID', $user->getID(), PDO::PARAM_INT);
        $query->bindParam(':keyID', $this->id, PDO::PARAM_INT);
        $query->execute();
    }

    //This method checks if a given User holds a Key or not, returning TRUE if they do
    //otherwise FALSE
    public function turn($user) {
        $db = self::getDB();
        $query = $db->prepare('SELECT * FROM userKeys WHERE userID = :userID AND keyID = :keyID');
        $query->bindParam(':userID', $user->getID(), PDO::PARAM_INT);
        $query->bindParam(':keyID', $this->id, PDO::PARAM_INT);
        $query->execute();
        $query->bindColumn('id', $id, PDO::PARAM_INT);
        $query->fetch(PDO::FETCH_BOUND);
        return $id !== NULL;
    }

    //This method removes the Key object from the database. Note that is not possible to
    //nullify the instance so it is up to the user of the library to do so!
    public function remove() {
        $db = self::getDB();
        $query = $db->prepare('DELETE FROM keys WHERE id = :id');
        $query->bindParam(':id', $this->id, PDO::PARAM_INT);
        $query->execute();
    }

    //This method returns an array or User objects representing Users which hold the Key
    public function getUsers() {
        $db = self::getDB();
        $query = $db->prepare('SELECT userID FROM userKeys WHERE keyID = :keyID');
        $query->bindParam(':keyID', $this->id, PDO::PARAM_INT);
        $query->execute();
        $query->bindColumn('userID', $userID, PDO::PARAM_INT);
        while($query->fetch(PDO::FETCH_BOUND)) {
            $users[] = new User($userID);
        }
        return $users;
    }

    //Adds a new key directly into the database
    public static function add($name, $description) {
        try {
            $db = self::getDB();
            $query = $db->prepare('INSERT INTO keys (name, description) VALUES (:name, :description)');
            $query->bindParam(':name', $name, PDO::PARAM_STR);
            $query->bindParam(':description', $description, PDO::PARAM_STR);
            $query->execute();
        }
        catch(PDOException $e) {
            if($e->getCode() == '23000')
                throw new KeyUnavailableNameException($name, $e);
            throw $e;
        }
    }

    //Returns an array of registered Keys, optionally limited to those a given User holds
    public static function getAll($user = NULL) {
        $db = self::getDB();
        if($user === NULL)
            $query = $db->query('SELECT id FROM keys ORDER BY id ASC');
        else {
            $query = $db->prepare('SELECT keyID AS id FROM userKeys WHERE userID = :userID ORDER BY keyID ASC');
            $query->bindParam(':userID', $user->getID(), PDO::PARAM_INT);
            $query->execute();
        }
        $query->bindColumn('id', $id, PDO::PARAM_INT);
        while($query->fetch(PDO::FETCH_BOUND)) {
            $keys[] = new Key($id);
        }
        return $keys;
    }

    //This method returns a PDO object representing the db used for Key data; currently
    //this defers directly to User as User & Key data must be in the same db. This
    //should always be used to access the Keys db
    public static function getDB() {
        return User::getDB();
    }

    //This method must be called at least once before Key can be used to setup the
    //database tables etc.
    public static function setupDB() {
        User::setupDB();
        $db = self::getDB();
        $db->exec(self::KEYS_SCHEMA);
        $db->exec(self::USERKEYS_SCHEMA);
        $db->exec(self::USERKEYSINDEX_SCHEMA);
    }
}

//CLASS SPECIFIC EXCEPTIONS FOLLOW
class KeyInvalidModeException extends DomainException {
    public function __construct($method, $mode) {
        parent::__construct("$method called with invalid mode flag: $mode");
    }
}

class KeyUnavailableNameException extends RuntimeException {
    public function __construct($name, $previous){
        parent::__construct(
            "A Key with the name '$name' already exists in database.",
            0, //Error code
            $previous
        );
    }
}

class KeyNoSuchKeyException extends OutOfBoundsException {
    public function __construct($uid, $mode) {
        switch($mode) {
            case Key::GET_BY_ID:
                $mode = 'id';
                break;
            case Key::GET_BY_NAME:
                $mode = 'name';
                break;
        }
        parent::__construct("Key with given $mode does not exist: $uid");
    }
}

class KeyUserAlreadyHasKeyException extends RuntimeException {
    public function __construct($user, $key, $previous) {
        parent::__construct(
            "User '$user' already has key '$key'",
            0, //Error code
            $previous
        );
    }
}

?>
